package streaming;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;

import simulation.Config;
import simulation.HostRequestedDiagnostic;
import simulation.Logs;
import simulation.MessageToComparatorModule;
import simulation.Simulator;
import simulation.TrackerPeriodicUpdate;
import simulation.TrackerRequest;

public class Tracker {

	/*	
	 *  T Set Structure:
	 *  
	 *  cid
	 *	 |__hash 1
	 *   |    |___ list of peers
	 *   |
	 *   |__hash 2
	 *   |    |___ list of peers
	 *   |
	 *   |__hash 3      
	 *   |    |___ list of peers
	 *   :
	 *   :
	 */
	
	HashMap<Integer, TrackerRequest> tSet;
	private static Tracker _instance;
	HashMap<Integer, Boolean> chunksToBeCompared; // K -> Integer that holds cid; V -> Boolean that inform if cid U Set was asked to peers
	TrackerPeriodicUpdate trackerPeriodicUpdate;
	HashMap<Integer, HashMap<Integer, Integer>> faultFreeDiagnostic; // k -> Integer that holds cid; V -> Arraylist  of peers ids
	HashMap<Integer, HashMap<Integer, Integer>> faultDiagnostic; // k -> Integer that holds cid; V -> Arraylist  of peers ids
	HashMap<Integer, HashMap<Integer, Integer>> notAnsweredDiagnostic; // k -> Integer that holds cid; V -> Arraylist  of peers ids
	
	private Tracker(){
		this.tSet = new HashMap<Integer, TrackerRequest>();
		this.chunksToBeCompared = new HashMap<Integer, Boolean>();
		
		// select chunks that will be compared
		this.selectChunksToBeCompared();
		
		// Initialize structures that stores the system diagnostic
		this.faultFreeDiagnostic = new HashMap<Integer, HashMap<Integer,Integer>>();
		this.faultDiagnostic = new HashMap<Integer, HashMap<Integer,Integer>>();
		this.notAnsweredDiagnostic = new HashMap<Integer, HashMap<Integer,Integer>>();
		
		// Creates update event
		this.trackerPeriodicUpdate = new TrackerPeriodicUpdate(this);
		this.updateStatus();
	}
	
	private void selectChunksToBeCompared(){
		int chunkIntervalSelectionCounter = 5; // the first chunk to be compared is 5, to give some time to peers know what chunks to compare
		
		while (chunkIntervalSelectionCounter < Config.TIME){
			if ((chunkIntervalSelectionCounter % StreamingConfig.TRACKER_CHUNK_SELECTION_INTERVAL) == 0){
				int cid = chunkIntervalSelectionCounter * StreamingConfig.MCASTRATE;
				this.getChunksToBeCompared().put(cid, false);
			}
			chunkIntervalSelectionCounter++;
		}
	}
	
	private ArrayList<Integer> getArrayListChunksToBeCompared(){
		ArrayList<Integer> chunks = new ArrayList<Integer>();
		
		for (Integer cid : this.getChunksToBeCompared().keySet()){
			chunks.add(cid);
		}
		
		return chunks;
	}
	
	/**
	 * Handle message received with peer's host grouping
	 * @param cid Chunk id.
	 * @param hostId Host id that sent the message with the U set.
	 * @param uSet Set with individual peer grouping done by comparator module.
	 */
	public void handleUSet(Integer hostId, Integer cid, HashMap<String, ArrayList<HostRequestedDiagnostic>> uSet){
		// Check if structure for cid do exist
		if (this.getTSet().get(cid) == null){
			// Creates a structure only to auxiliate the creation of TSet for this cid
			TrackerRequest aux = new TrackerRequest(cid);
			
			// If doesn't exist, creates it
			this.getTSet().put(cid, aux);
			
			// Start timeout
			this.getTSet().get(cid).setTimeout(Simulator.getInstance().getTime() + (2*StreamingConfig.MCASTWINDOWSIZE));
		}

		// gets T set for cid
		TrackerRequest tCid = this.getTSet().get(cid);
		
		if (!tCid.timeoutSet){
			this.getTSet().get(cid).setTimeout(Simulator.getInstance().getTime() + (2*StreamingConfig.MCASTWINDOWSIZE));
			tCid.timeoutSet = true;
		}
		
		// iterate over U set sent by peer
		for (String hash : uSet.keySet()){
			ArrayList<HostRequestedDiagnostic> hdrList = uSet.get(hash);

			// If groups doesn't exit, creates and insert list of hosts
			if (tCid.getHostsList().get(hash) == null){
				tCid.getHostsList().put(hash, hdrList);
			}
			else {
				// Iterate through list of host inside 
				for (Object hostReqDiagObject : hdrList.toArray()){
					HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic

					// Insert hdr on tCid's hash subgroup if isn't there yet
					if (!this.alreadyInserted(cid, hash, hdr.getHostId())){
						tCid.getHostsList().get(hash).add(hdr);
					}
				}// end for iteration that inserts each host in the group
			} // end of if statement to check existence of hash subset
		} // end of for iteration over U set
		
		// Sets true to host, meaning it sent U Set for cid
		this.getTSet().get(cid).getHostIdsSentUSet().put(hostId, true);
		
		Logs.printMessage("Tracker", "[MSGTR1]" + hostId + " sent U set of cid " + cid + ".");
		
	}

	/**
	 * Check if hostId was already insert in T set with cid
	 * @param cid Chunk if of the set.
	 * @param hash Hash of the subset inside set with cid.
	 * @param hostId Host id that is going to be checked
	 * @return Return true if host is already in T Set, false otherwise.
	 */
	private boolean alreadyInserted(Integer cid, String hash, Integer hostId){
		boolean inserted = false;
		
		for (Object hostReqDiagObject : this.getTSet().get(cid).getHostsList().get(hash).toArray()){
			HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic

			// If ids are equal, host is already in set
			if (hdr.getHostId().equals(hostId)){
				inserted = true;
				break;
			}
		}
		return inserted;
	}
	
	/**
	 * Handle message sent by source host informing the last chunk generated and propagated
	 * @param cid Chunk if of the chunk generated by source
	 */
	public void handleChunkPropagation(Integer cid) {
		// If chunk generated by source is one of the chunks chosen to be compared, inform all comparator modules
		// Second comparison checks if cid was already requested to peers
		if (this.getChunksToBeCompared().containsKey(cid) && this.getChunksToBeCompared().get(cid) == false){
			Overlay overlay = Overlay.getInstance(); 
				
			Object[] hostsList = overlay.hosts.values().toArray();
			
			// if structure haven't been created
			if (this.getTSet().get(cid) == null){
				this.getTSet().put(cid, new TrackerRequest(cid));
				
				// Start timeout
				this.getTSet().get(cid).setTimeout(Simulator.getInstance().getTime() + (2*StreamingConfig.MCASTWINDOWSIZE));
			}
			
			// Starts in 1 to avoid source
			for (int i = 1; i < hostsList.length; i++){
				Host auxHost = (Host)hostsList[i];
				// Set that auxHost was requested, and false means this host haven't answered
				this.getTSet().get(cid).getHostIdsSentUSet().put(auxHost.getId(), false);
				Logs.printMessage("Tracker", "[MSGTR2] Tracker inserted " + auxHost.getId() + " as one of peers that must send the U Set of cid " + cid + ".");
			}
			
			// Updates status of cid as true (was asked to all peers
			this.getChunksToBeCompared().put(cid, true);
		}
	}
	
	/**
	 * Update method
	 */
	public void updateStatus() {
		this.getTrackerPeriodicUpdate().addEvent();
		
		for (TrackerRequest tr : this.getTSet().values()){
			
			// If timeout to await U sets has reached, finalizes T set
			if (!tr.isDiagnosticFinished() && tr.timeoutSet){
				if (this.allPeersAnswered(tr)) {
					tr.setDiagnosticFinished(true);
					this.printTSubSet(tr);
					this.executeSubsetDiagnostic(tr);
				}
				else if (tr.getTimeout() < Simulator.getInstance().getTime()) {
					tr.finalizeGrouping();
					this.printTSubSet(tr);
					this.executeSubsetDiagnostic(tr);
				}
			}
		}
	}
	
	/**
	 * Check if all peers answered tracker and sent U Set for cid
	 * @param tr The Tracker Request controller for cid
	 * @return Return true if all peers have answered the request, false otherwise
	 */
	private boolean allPeersAnswered(TrackerRequest tr) {
		boolean allAnswered = true;
		
		// Iterate over all hosts that was requested
		for (Integer hostId : tr.getHostIdsSentUSet().keySet()){
			// If host sent U set
			if (!tr.getHostIdsSentUSet().get(hostId)){
				allAnswered = false;
				break;
			}
		}
		return allAnswered;
	}
	
	/**
	 * Print all T Set
	 */
	@SuppressWarnings("unchecked")
	public void printTSet() {
		SortedSet<Integer> sortedSet = new TreeSet<Integer>(this.getTSet().keySet());
		
		Logs.printMessage("Tracker_T_Set", "\n\n********** Tracker results **********");
		// Iterate over chunk ids group
//		for (Integer cid : this.getTSet().keySet()){
		Iterator<Integer> it = sortedSet.iterator();
		
		while (it.hasNext()){
			Integer cid = it.next();
			Logs.printMessage("Tracker_T_Set", "********** CID " + cid + " **********");
			
			int simpleCounter = 0;
			for (String hash : this.getTSet().get(cid).getHostsList().keySet()){
				Logs.printMessage("Tracker_T_Set", "+++++ Hash " + ++simpleCounter + ": " + hash);
				ArrayList<HostRequestedDiagnostic> hdrList = this.getTSet().get(cid).getHostsList().get(hash);
	
				// Sort hosts
				Collections.sort(hdrList, new Comparator(){
					 
		            public int compare(Object o1, Object o2) {
		            	HostRequestedDiagnostic p1 = (HostRequestedDiagnostic) o1;
		            	HostRequestedDiagnostic p2 = (HostRequestedDiagnostic) o2;
		               return p1.getHostId().compareTo(p2.getHostId());
		            }
		        });
				
				String strHosts = new String("Hosts: [");
				
				// Iterate through list of host inside 
				for (Object hostReqDiagObject : hdrList.toArray()){
					HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
					strHosts += hdr.getHostId() + ",";
				} // end for iteration that inserts each host in the group
				strHosts += "]";
				
				Logs.printMessage("Tracker_T_Set", strHosts);
			} // end of for iteration over U set
			printDiagnostic(cid);
		} // enf of for iteration over cids
	}
	
	/**
	 * Diagnostic subset of T 
	 * @param tr The subset of T that will be diagnosticated
	 */
	private void executeSubsetDiagnostic(TrackerRequest tr) {
		String hashSource = "";
		String haventAnswered = "Haven't answered";
		ArrayList<HostRequestedDiagnostic> hrdList = new ArrayList<HostRequestedDiagnostic>();
		
		// Check fault ones
		
		for (String hash : tr.getHostsList().keySet()){
			// If hash isn't for peers that haven't answered, continues. Otherwise, skip...
			if (!hash.equalsIgnoreCase(haventAnswered)){
				hrdList = tr.getHostsList().get(hash);
				
				for (Object hostReqDiagObject : hrdList.toArray()){
					HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
					
					// Check if the source is within this set. If true, store the hash of this set
					if (hdr.getHostId().equals(0)){
						hashSource = hash;
					}
					
					if (checkRepetition(tr, hdr.getHostId(), hash, true) || !isSourceSet(hrdList)){
						if (this.getFaultDiagnostic().get(tr.getCid()) == null){
							//create structure
							this.getFaultDiagnostic().put(tr.getCid(), new HashMap<Integer,Integer>());
						}
						
						// Adds host in list of faulty hosts
						this.getFaultDiagnostic().get(tr.getCid()).put(hdr.getHostId(), hdr.getHostId());
					}
				} // end for iteration that inserts each host in the group
			}
		}
		hrdList = null;
		
		// check fault free
		if (hashSource != ""){
			hrdList = tr.getHostsList().get(hashSource);
			
			for (Object hostReqDiagObject : hrdList.toArray()){
				HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
				
				// If don't repeat in any other set, then, it is fault-free
				if (!checkRepetition(tr, hdr.getHostId(), hashSource, true)){
					if (this.getFaultFreeDiagnostic().get(tr.getCid()) == null){
						//create structure
						this.getFaultFreeDiagnostic().put(tr.getCid(), new HashMap<Integer,Integer>());
					}
					
					// Adds host in list of faulty hosts
					this.getFaultFreeDiagnostic().get(tr.getCid()).put(hdr.getHostId(), hdr.getHostId());
				}
			}
		}
		hrdList = null;
		
		// check peers that haven't answered
		hrdList = tr.getHostsList().get(haventAnswered);
		
		if (hrdList != null){
			for (Object hostReqDiagObject : hrdList.toArray()){
				HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
				
				// If doesn't repeat in any other subset, only on the haven't answered one, than inserts it at the structure
				if (!checkRepetition(tr, hdr.getHostId(), haventAnswered, false)){
					if (this.getNotAnsweredDiagnostic().get(tr.getCid()) == null){
						//create structure
						this.getNotAnsweredDiagnostic().put(tr.getCid(), new HashMap<Integer,Integer>());
					}
					
					// Adds host in list of faulty hosts
					this.getNotAnsweredDiagnostic().get(tr.getCid()).put(hdr.getHostId(), hdr.getHostId());
				}
			}
		}
	}
	
	private boolean isSourceSet(ArrayList<HostRequestedDiagnostic> hrdList) {
		boolean sourceSet = false;
		
		for (HostRequestedDiagnostic hrd : hrdList){
			if (hrd.getHostId().equals(new Integer(0))){
				sourceSet = true;
				break;
			}
		}
		
		return sourceSet;
	}

	/**
	 * Check if a host sent different chunks to different neighbors, being put in two different sets. If
	 * the peer repeats, that means, is in two different sets, he may be a polluter.
	 * @param tr The T subset for chunk being verified
	 * @param id Id of the peer that is being compared
	 * @param originalHash The subset's hash that peer is inside
	 * @return Return true if peer is within two different sets, false otherwise
	 */
	private boolean checkRepetition(TrackerRequest tr, Integer id, String originalHash, boolean removeNotAnswered){
		boolean repeat = false;
		TrackerRequest trAux = new TrackerRequest(tr);
		trAux.getHostsList().remove(originalHash);
		
		// 
		String haventAnswered = "Haven't answered";
		if (removeNotAnswered && tr.getHostsList().get(haventAnswered) != null){
			trAux.getHostsList().remove(haventAnswered);
		}
		
		// TODO se esta no cjt do fonte mas tb no havent answered, esta falho?
		for (String hash : trAux.getHostsList().keySet()){
			ArrayList<HostRequestedDiagnostic> hdrList = trAux.getHostsList().get(hash);
			
			for (Object hostReqDiagObject : hdrList.toArray()){
				HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
				if (id.equals(hdr.getHostId())){
					repeat = true;
					break;
				}
			} // end for iteration that inserts each host in the group
		}
		
		return repeat;
	}
	
	/**
	 * Print subset diagnostic 
	 * @param cid Chunk id of the set to be printed
	 */
	private void printDiagnostic(Integer cid){
		String diagnostic = "Diagnostic of T(" + cid + ") = ";
		
		// fault free
		if (this.getFaultFreeDiagnostic().get(cid) != null){ // if to avoid any null pointer errors
			diagnostic += "FAULT-FREE[";
			
			SortedSet<Integer> sortedSet = new TreeSet<Integer>(this.getFaultFreeDiagnostic().get(cid).values());
			
			Iterator<Integer> it = sortedSet.iterator();
			
			while (it.hasNext()){
//			for (Integer id : this.getFaultFreeDiagnostic().get(cid).values()){
				diagnostic += it.next() + ",";
			}
			diagnostic = diagnostic.substring(0, diagnostic.length()-1) + "]"; //removes last comma and inserts ']'
		}
		
		// faulty
		if (this.getFaultDiagnostic().get(cid) != null){ // if to avoid any null pointer errors
			diagnostic += "FAULTY[";
			
			SortedSet<Integer> sortedSet = new TreeSet<Integer>(this.getFaultDiagnostic().get(cid).values());
			
			Iterator<Integer> it = sortedSet.iterator();
			
			while (it.hasNext()){
//			for (Integer id : this.getFaultDiagnostic().get(cid).values()){
				diagnostic += it.next() + ",";
			}
			diagnostic = diagnostic.substring(0, diagnostic.length()-1) + "]"; //removes last comma and inserts ']'
		}
		
		// not answered
		if (this.getNotAnsweredDiagnostic().get(cid) != null){ // if to avoid any null pointer errors
			diagnostic += "NOTANSWERED[";
			
			SortedSet<Integer> sortedSet = new TreeSet<Integer>(this.getNotAnsweredDiagnostic().get(cid).values());
			
			Iterator<Integer> it = sortedSet.iterator();
			
			while (it.hasNext()){
//			for (Integer id : this.getNotAnsweredDiagnostic().get(cid).values()){
				diagnostic += it.next() + ",";
			}
			diagnostic = diagnostic.substring(0, diagnostic.length()-1) + "]"; //removes last comma and inserts ']'
		}
		
		diagnostic += "\n";
		
		Logs.printMessage("Tracker_T_Set", diagnostic);
		
	}
	
	/**
	 * Print subset of T when all peers sent U sets or timeout was reached
	 * @param tr The set of cid (subset of T) that will be printed
	 */
	private void printTSubSet(TrackerRequest tr) {
		int simpleCounter = 0;
		for (String hash : tr.getHostsList().keySet()){
			Logs.printMessage("Tracker", "+++++ Hash " + ++simpleCounter + ": " + hash);
			ArrayList<HostRequestedDiagnostic> hdrList = tr.getHostsList().get(hash);

			String strHosts = new String("Hosts: [");
			
			// Iterate through list of host inside 
			for (Object hostReqDiagObject : hdrList.toArray()){
				HostRequestedDiagnostic hdr = (HostRequestedDiagnostic) hostReqDiagObject; // cast from Object to HostRequestedDiagnostic
				strHosts += hdr.getHostId() + ",";
			} // end for iteration that inserts each host in the group
			strHosts += "]";
			Logs.printMessage("Tracker", strHosts);
		} // end of for iteration over U set
	}
	
	/**
	 * Sends to peer that requested the list of chunks to be compared during execution
	 * @param requester The peer that requested this information.
	 */
	public void handlePeerRequestForChunksToBeCompared(Host requester){
		// Send message informing that cid must be compared by comparator modules
		new MessageToComparatorModule("chunksToBeCompared", requester, this.getArrayListChunksToBeCompared());
//		Simulator.getInstance().addIntDiagnosticRequestMessages();
	}
	
	/**
	 * Get an instance of T Set
	 * @return An instance of T Set
	 */
	public static Tracker getInstance() {
		if (_instance == null){
			_instance = new Tracker();
		}
		return _instance;
	}
	
	public static void resetTracker() {
		_instance = null;
	}
	
	/* Getters and Setters */
	
	public HashMap<Integer, TrackerRequest> getTSet() {
		return tSet;
	}

	public void setTSet(HashMap<Integer, TrackerRequest> tSet) {
		this.tSet = tSet;
	}

	public HashMap<Integer, Boolean> getChunksToBeCompared() {
		return chunksToBeCompared;
	}

	public void setChunksToBeCompared(HashMap<Integer, Boolean> chunksToBeCompared) {
		this.chunksToBeCompared = chunksToBeCompared;
	}

	public TrackerPeriodicUpdate getTrackerPeriodicUpdate() {
		return trackerPeriodicUpdate;
	}

	public void setTrackerPeriodicUpdate(TrackerPeriodicUpdate trackerPeriodicUpdate) {
		this.trackerPeriodicUpdate = trackerPeriodicUpdate;
	}

	public HashMap<Integer, HashMap<Integer,Integer>> getFaultFreeDiagnostic() {
		return faultFreeDiagnostic;
	}

	public void setFaultFreeDiagnostic(
			HashMap<Integer, HashMap<Integer,Integer>> faultFreeDiagnostic) {
		this.faultFreeDiagnostic = faultFreeDiagnostic;
	}

	public HashMap<Integer,HashMap<Integer,Integer>> getFaultDiagnostic() {
		return faultDiagnostic;
	}

	public void setFaultDiagnostic(
			HashMap<Integer, HashMap<Integer,Integer>> faultDiagnostic) {
		this.faultDiagnostic = faultDiagnostic;
	}

	public HashMap<Integer, HashMap<Integer,Integer>> getNotAnsweredDiagnostic() {
		return notAnsweredDiagnostic;
	}

	public void setNotAnsweredDiagnostic(
			HashMap<Integer, HashMap<Integer,Integer>> notAnsweredDiagnostic) {
		this.notAnsweredDiagnostic = notAnsweredDiagnostic;
	}
}